import os
import hashlib
import re
import shutil
import time
import stat
import subprocess

catch_axel_status_re = re.compile(r"\[\s{0,}(\d+)%\](\s{0,}\.{10}\s{0,}){5}\[\s{0,}(\d+.\d+)KB/s\]")
axel_finish_re = re.compile(r"Downloaded \d{0,}.{0,}\d{0,} kilobytes in \d+ seconds.")


class UranomeUpdateEngine():
    """
    最好是只使用一个清单，多个清单的支持目前还有一些问题
    """
    def __init__(self, process_cb=None):
        """
        :param process_cb void(doing, file_path, progress[int], speed[float])
        """
        super(UranomeUpdateEngine, self).__init__()
        self._process_cb = process_cb

        self.manifests = []
        self._manifest_patch = None

        # self.sigProcessing.connect(self.self_test)
        self.active_axel_process = None
        self.downloading_file = ""
        self.active_axel_output = ""
        self._last_match = None

        self._trans = {"checking": "checking", "downloading": "downloading", "extracting": "extracting"}

    def add_manifest(self, manifest_file, base_url):
        if base_url.endswith("/") is False:
            base_url += "/"
        self.manifests.append( (manifest_file, base_url) )

    def add_patch(self, manifest_file, base_url):
        if base_url.endswith("/") is False:
            base_url += "/"
        self._manifest_patch = (manifest_file, base_url)

    def show_axel_output(self):
        while True:
            try:
                this_time_output, this_time_error = self.active_axel_process.communicate(timeout=15)
                print("output: {0}, error: {1}".format(this_time_output.decode(), this_time_error.decode()))
            except ValueError as e:
                if "closed file" in str(e):
                    break
            self.active_axel_output += this_time_output.decode()
            if axel_finish_re.search(self.active_axel_output) is not None:
                break
            #找下下载的速度标签
            matches = catch_axel_status_re.findall(self.active_axel_output)
            if len(matches) is not 0:
                last_match = matches[-1]
                if last_match == self._last_match:
                    return
                else:
                    self._last_match = last_match
                downloading_tran = self._trans["downloading"]
                try:
                    if self._process_cb:
                        self._process_cb(downloading_tran, self.downloading_file, int(last_match[0]), float(last_match[-1]))
                except:
                    import traceback
                    traceback.print_exc()

    def set_doing_translate(self, trans):
        self._trans.update(trans)

    def start(self, common_dir=".", dst_dir=".", quick_mode = ""):
        """
        :param common_dir: 通用工具放哪在？请不要在末尾加"/"
        :param dst_dir: 清单中的文件将要放哪？请不要在末尾加"/"
        :param quick_mode: 快速模式清单文件名
        """
        quick_mode_manifest = None
        if len(quick_mode) != 0:  # 快速模式
            import pickle
            try:
                quick_mode_manifest_file = open(dst_dir+"/"+quick_mode, "rb")
                quick_mode_manifest = pickle.load(quick_mode_manifest_file)
                quick_mode_manifest_file.close()
            except (FileNotFoundError, EOFError):
                pass
        finish_re = re.compile(r"Downloaded [0-9.]{1,} (megabytes)|(kilobytes)|(bytes) in \d+ seconds.")
        _7za_path = common_dir+"/addon/7za"
        axel_path = common_dir+"/addon/axel"
        extract_items = []  # (压缩包路径,要解压到的工作路径,原始摘要)
        # try:
        #     shutil.rmtree("cache")
        # except FileNotFoundError as e:
        #     pass
        # 统计总文件,届时下载占90%,解压占10%
        total_item_num = 0
        for manifest_file, base_url in self.manifests:
            total_item_num += len(manifest_file.items())
        # 先全部下载下来
        item_cursor = 1
        for manifest_file, base_url in self.manifests:
            for file_path, file_prop in manifest_file.items():
                need_download = False
                if quick_mode_manifest is not None:  # 快速模式下，直接对比快速模式清单
                    # 检测
                    checking_tran = self._trans["checking"]
                    if self._process_cb:
                        self._process_cb(checking_tran, file_path, (item_cursor/total_item_num)*100*0.9, -1)
                    quick_mode_file_prop = quick_mode_manifest.get(file_path)
                    if quick_mode_file_prop is None:
                        need_download = True
                    elif quick_mode_file_prop == file_prop:
                        need_download = False
                    else:
                        need_download = True
                else:
                    #与本地的此文件对比
                    local_file_size = -1
                    #检测
                    checking_tran = self._trans["checking"]
                    if self._process_cb:
                        self._process_cb(checking_tran, file_path, (item_cursor/total_item_num)*100*0.9, -1)
                    try:
                        local_file_size = os.path.getsize(dst_dir+"/"+file_path)
                    except FileNotFoundError as e:
                        pass
                    if file_prop["size"] != local_file_size:  # 大小不同,确定下载
                        need_download = True
                    elif len(quick_mode) == 0:  # 非快速模式，比较摘要值
                        md5er = hashlib.md5()
                        md5er.update(open(dst_dir+"/"+file_path, "rb").read())
                        local_file_digest = md5er.digest()
                        if local_file_digest != file_prop["digest"]:
                            need_download = True
                # print("local size:", local_file_size, "prop size:", file_prop["size"])
                if need_download:
                    print("need download")
                    download_file_path = dst_dir+"/cache/"+file_path+".xz"
                    download_file_dir = os.path.dirname(download_file_path)
                    #创建下载文件存放路径
                    try:
                        os.makedirs(download_file_dir)
                    except FileExistsError as e:
                        pass
                    #下载
                    file_url = base_url + file_path + ".xz"
                    file_url = file_url.replace("+","_")
                    print("url:", file_url)
                    arg = '{0} "{1}" -o "{2}"'.format(axel_path, file_url, download_file_path)
                    work_progress = subprocess.Popen(arg, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                    self.active_axel_output = ""
                    self.active_axel_process = work_progress
                    self.downloading_file = file_path

                    self.show_axel_output()

                    extract_items.append( (download_file_path, os.path.dirname(dst_dir+"/"+file_path), file_prop["digest"]) )
                item_cursor += 1
        #解压
        _7za_process = None
        extract_item_cursor = 1
        for extract_item_path, extract_to_dir,remote_digest in extract_items:
            #创建解压目录
            #TODO: 没有进行摘要验证
            if len(extract_to_dir) != 0:  # 空目录,不需要构造了
                try:
                    os.makedirs(extract_to_dir)
                except FileExistsError as e:
                    pass
            else:
                extract_to_dir = "./"
            extracting_tran = self._trans["extracting"]
            if self._process_cb:
                self._process_cb(extracting_tran, extract_item_path, int(90+extract_item_cursor/len(extract_items)*10), -1)

            _7za_process = subprocess.Popen('{0} x -y "{1}" -o"{2}"'.format(_7za_path, extract_item_path, extract_to_dir),
                                            stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
            _7za_process.wait()
            _7za_process_output, _7za_process_error = _7za_process.communicate(timeout=5)
            print("7za output:", _7za_process_output)
            if extract_item_path.endswith(".to_be_extract.xz") is True:  # 包裹文件
                if self._manifest_patch is not None:
                    total_item_num = len(self._manifest_patch[0])
                while True:
                    _7za_process = subprocess.Popen('{0} x -y "{1}" -o"{2}"'.format(_7za_path,
                                                                       extract_to_dir+"/"+os.path.basename(extract_item_path)[:-3],
                                                                       extract_to_dir),
                                                    stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
                    _7za_process.wait()
                    _7za_process_output, _7za_process_error= _7za_process.communicate(timeout=5)
                    print("7za output:", _7za_process_output)

                    # 是这样的，有一种情况，就是xz包解压正确，得到正确的包裹文件，然后之后包裹文件解压错误了
                    # 这种情况的话必须对解压出的文件进行全部校验才能得知
                    # 我们通过增加一个包裹文件的patch清单进行校验
                    item_cursor = 0
                    if self._manifest_patch is not None:
                        for file_path, file_prop in self._manifest_patch[0].items():
                            #与本地的此文件对比
                            local_file_size = -1
                            #检测
                            checking_tran = self._trans["checking"]
                            if self._process_cb:
                                self._process_cb(checking_tran, file_path, (item_cursor/total_item_num)*100, -1)
                            try:
                                local_file_size = os.path.getsize(dst_dir+"/"+file_path)
                            except FileNotFoundError as e:
                                pass
                            if file_prop["size"] != local_file_size:  # 大小不同,重新解压
                                break
                            elif len(quick_mode) == 0:  # 非快速模式，比较摘要值
                                md5er = hashlib.md5()
                                md5er.update(open(dst_dir+"/"+file_path, "rb").read())
                                local_file_digest = md5er.digest()
                                if local_file_digest != file_prop["digest"]:
                                    break
                            item_cursor += 1
                        else:
                            break
                    else:
                        break
            extract_item_cursor += 1
        #删除
        try:
            shutil.rmtree(dst_dir+"/cache")
        except (FileNotFoundError, OSError) as e:
            pass
        if self._process_cb:
            self._process_cb("starting", "", 100, -1)
        if len(quick_mode) != 0:  # 快速模式把清单留在现场
            import pickle
            quick_mode_manifest_file = open(dst_dir+"/"+quick_mode, "wb")
            pickle.dump(self.manifests[0][0], quick_mode_manifest_file)
            quick_mode_manifest_file.close()

def prepare_common_components(dst_dir="."):
    # 我需要部署基本ue引擎需要的component
    import os
    import hashlib
    import platform
    try:
        os.mkdir(dst_dir+"/addon")
    except FileExistsError:
        pass
    platform_name = platform.platform().lower()
    if "win" in platform_name:
        addons_manifest = {
            "addon/7za.exe": (587776
                , "42badc1d2f03a8b1e4875740d3d49336"
                , "http://uranome.oss-cn-beijing.aliyuncs.com/component/7z/7za/9.20/win32/7za.exe")
            , "addon/cygwin1.dll": (2648181
                , "bc68345f873de78310d324320e82fe65"
                , "http://uranome.oss-cn-beijing.aliyuncs.com/component/cygwin/1.7.7/win32/cygwin1.dll")
            , "addon/axel.exe": (31232
                , "df396d53ca2ff61f96f2ad9489ca5bfb"
                , "http://uranome.oss-cn-beijing.aliyuncs.com/component/axel/2.4/win32/axel.exe"
            )
        }
    elif "linux" in platform_name:
        addons_manifest = {
            "addon/7za": (1531424
                , "97450f98e023504215f8c4ef2b149754"
                , "http://uranome.oss-cn-beijing.aliyuncs.com/files/component/7z/7za/9.38.1/linux/7za")
            , "addon/axel": (31876
                , "0a720c624c7721734d07944a3360431d"
                , "http://uranome.oss-cn-beijing.aliyuncs.com/files/component/axel/2.4.1/linux/axel"
            )
        }
    else:
        raise NotImplementedError

    # 校验组件
    for addon_name, addon_prop in addons_manifest.items():
        get_component_from_remote = False
        try:
            component_stat = os.stat(dst_dir+"/"+addon_name)
            if component_stat.st_size != addon_prop[0]:  # 大小不同，需要更新
                get_component_from_remote = True
            else:  # 大小相同,比较摘要
                component_file = open(dst_dir+"/"+addon_name, "rb")
                md5er = hashlib.md5()
                md5er.update(component_file.read())
                component_compute_hash = md5er.hexdigest()
                if component_compute_hash != addon_prop[1]:
                    get_component_from_remote = True
        except FileNotFoundError as e:
            # 本地本文件不存在
            get_component_from_remote = True
        if get_component_from_remote is True:
            from urllib.request import urlretrieve

            for i in range(0, 100):
                try:
                    urlretrieve(addon_prop[2], dst_dir+"/"+addon_name)
                    break
                except Exception as e:
                    if "name resolution" in str(e):
                        pass
                    else:
                        raise e

                    time.sleep(1)
        # if this is linux, we should chmod for this
        if "linux" in platform_name:
            try:
                os.chmod(dst_dir+"/"+addon_name, 0o775)
            except PermissionError as e:
                pass